﻿/*******************************************************************************
 * AAEngine
 * Copyright (c) 2010 Mike Jarosch
 *
 * Original source PushButton Engine:
 * Copyright (C) 2009 PushButton Labs, LLC
 * For more information see http://www.pushbuttonengine.com
 *
 * This file is licensed under the terms of the MIT license, which is included
 * in the "MIT License.txt" file at the root directory of this SDK.
 ******************************************************************************/

using System;
using System.Collections.Generic;
using System.Xml.Linq;
using AAEngine.Engine.Debug;
using AAEngine.Engine.Entities;
using AAEngine.Engine.Resources;
using AAEngine.Engine.Serialization;

namespace AAEngine.Engine.Core
{
    /// <summary>
    /// The LevelManager allows level files and groups to be added to a specific level so they
    /// can be automatically managed when that level is loaded or unloaded.
    /// </summary>
    public class LevelManager : EventDispatcher, ISerializable
    {
        /// <summary>
        /// Returns the number of levels the LevelManager has data for.
        /// </summary>
        public int LevelCount
        {
            get { return _levelDescriptions.Count; }
        }

        public int CurrentLevel
        {
            get { return _currentLevel; }
        }

        /// <summary>
        /// Gets an array of filenames that are to be loaded with a specific level.
        /// </summary>
        public IEnumerable<string> GetLevelFiles(int index)
        {
            return _levelDescriptions[index].Files;
        }

        /// <summary>
        /// Gets an array of group names that are to be loaded with a specific level.
        /// </summary>
        public IEnumerable<string> GetLevelGroups(int index)
        {
            return _levelDescriptions[index].Groups;
        }

        /// <summary>
        /// Gets a level index based on the registered level name.  Case sensitive.
        /// </summary>
        /// <returns>Returns -1 if the level name is not found.</returns>
        public int GetLevelIndexByName(string levelName)
        {
            foreach (LevelDescription level in _levelDescriptions.Values)
            {
                if (level.Name == levelName)
                {
                    return level.Index;
                }
            }
            return -1;
        }

        ///// <summary>
        ///// With this method, you can override the default file loading methods by having
        ///// custom functions called instead of the defaults.
        ///// </summary>
        ///// <param name="load"></param>
        ///// <param name="unload"></param>
        public void SetLoadFileCallbacks(Action<string, Action> load, Action<string> unload)
        {
            _loadFileCallback = load;
            _unloadFileCallback = unload;
        }

        ///// <summary>
        ///// With this method, you can override the default group loading methods by having
        ///// custom functions called instead of the defaults.
        ///// </summary>
        ///// <param name="load"></param>
        ///// <param name="unload"></param>
        public void SetLoadGroupCallbacks(Action<string> load, Action<string> unload)
        {
            _loadGroupCallback = load;
            _unloadGroupCallback = unload;
        }

        /// <summary>
        /// Starts up the LevelManager. This is automatically called if level descriptions are loaded
        /// from a file with the Load method. Otherwise, this should be called after all the level
        /// data has been registered with AddFileReference and AddGroupReference. When everything is
        /// ready (except for the initialLevel loading), LevelEvent.READY_EVENT is dispatched.
        /// </summary>
        /// <param name="initialLevel">The level to load once everything is set up</param>
#if WINDOWS_PHONE || XBOX
        public void Start()
        {
            Start(-1);
        }

        public void Start(int initialLevel)
#else
        public void Start(int initialLevel = -1)
#endif
        {
            if (_isReady)
            {
                Logger.Error(this, "Start", "The LevelManager has already been started.");
                return;
            }

            AAGame.TemplateManager.AddEventListener(TemplateManager.FILE_LOADED_EVENT, OnFileLoaded);

            _isReady = true;
            DispatchEvent(new LevelEvent(LevelEvent.LEVEL_READY_EVENT, -1));

            if (initialLevel != -1)
            {
                LoadLevel(initialLevel);
            }
        }

        /// <summary>
        /// Starts up the level manager and loads level description data from an xml file. Start is
        /// automatically called when this finishes.
        /// </summary>
        /// <param name="filename">The file to load level descriptions from</param>
        /// <param name="initialLevel">The level to load once everything is set up</param>
#if WINDOWS_PHONE || XBOX
        public void Load(string filename)
        {
            Load(filename, -1);
        }

        public void Load(string filename, int initialLevel)
#else
        public void Load(string filename, int initialLevel = -1)
#endif
        {
            if (_isReady)
            {
                Logger.Error(this, "Load", "The LevelManager has already been started.");
                return;
            }

            XmlResource resource = (XmlResource)AAGame.ResourceManager.Load(filename, typeof(XmlResource));

            Serializer.Deserialize(this, resource.Data);

            Logger.Print(this, "Loaded " + _levelDescriptions.Count + " level descriptions.");

            Start(initialLevel);
        }

        /// <summary>
        /// Unload the current level.
        /// </summary>
        public void Clear()
        {
            UnloadCurrentLevel();

            _currentLevel = 0;
            _levelDescriptions.Clear();
            _isReady = false;
        }

        public void Serialize(XElement xml)
        {
            foreach (LevelDescription level in _levelDescriptions.Values)
            {
                XElement node = xml.AddElement("level");
                node.SetAttribute("index", level.Index);
                node.SetAttribute("name", level.Name);

                foreach (string filename in level.Files)
                {
                    XElement file = node.AddElement("file");
                    file.SetAttribute("filename", filename);
                }

                foreach (string groupname in level.Groups)
                {
                    XElement group = node.AddElement("group");
                    group.SetAttribute("groupname", groupname);
                }
            }
        }

        public object Deserialize(XElement xml)
        {
            foreach (XElement level in xml.Elements("level"))
            {
                int index = Int32.Parse(level.GetAttribute("index"));
                LevelDescription levelDescription = new LevelDescription();
                levelDescription.Index = index;
                levelDescription.Name = level.GetAttribute("name");
                _levelDescriptions[index] = levelDescription;

                foreach (XElement child in level.Elements())
                {
                    if (child.Name == "file")
                    {
                        AddFileReference(index, child.GetAttribute("filename"));
                    }
                    else if (child.Name == "group")
                    {
                        AddGroupReference(index, child.GetAttribute("name"));
                    }
                    else
                    {
                        Logger.Error(this, "Deserialize", "Encountered unknown tag '" + child.Name + "' in level description.");
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Loads an entity with the TemplateManager and tracks it. If the current level ends before the
        /// entity is destroyed, it will be destroyed automatically.
        /// </summary>
        /// <param name="name">The name of the entity to instantiate.</param>
        /// <returns>The created entity.</returns>
        public IEntity LoadEntity(string name)
        {
            if (!_isReady)
            {
                Logger.Error(this, "LoadEntity", "Cannot load entities. The LevelManager has not been started.");
                return null;
            }

            if(!_isLevelLoaded)
            {
                Logger.Error(this, "LoadEntity", "Cannot load entities. No level is loaded.");
                return null;
            }

            IEntity entity = AAGame.TemplateManager.InstantiateEntity(name);
            if(entity == null)
            {
                Logger.Error(this, "LoadEntity", "Failed to instantiate an entity with name '" + name + "'.");
                return null;
            }

            entity.EventDispatcher.AddEventListener(Entity.ENTITY_DESTROYED_EVENT, OnEntityDestroyed);
            _loadedEntities.Add(entity);

            return entity;
        }

        /// <summary>
        /// Loads the specified level and unloads any previous levels. If the previous level and the new
        /// level share data, it will not be reloaded.
        /// </summary>
        /// <param name="index">The level number to load.</param>
        /// <param name="force">Force reload</param>
#if WINDOWS_PHONE || XBOX
        public void LoadLevel(int index)
        {
            LoadLevel(index, false);
        }

        public void LoadLevel(int index, bool force)
#else
        public void LoadLevel(int index, bool force = false)
#endif
        {
            if (!_isReady)
            {
                Logger.Warn(this, "LoadLevel", "Warning: trying to loadLevel() without having first called start()! This probably won't work.");
            }

            if (!HasLevelData(index))
            {
                Logger.Error(this, "LoadLevel", "Level data for level " + index + " does not exists.");
                return;
            }

            List<string> filesToLoad = new List<string>();
            List<string> filesToUnload = new List<string>();
            List<string> groupsToUnload = new List<string>();

            if (force)
            {
                filesToLoad = _levelDescriptions[index].Files;
                _groupsToLoad = _levelDescriptions[index].Groups;

                if (HasLevelData(_currentLevel))
                {
                    filesToUnload = _levelDescriptions[_currentLevel].Files;
                    groupsToUnload = _levelDescriptions[_currentLevel].Groups;
                }
            }
            else
            {
                bool doUnload = _isLevelLoaded && _currentLevel > 0;
                GetLoadLists(doUnload ? _levelDescriptions[_currentLevel].Files : null, _levelDescriptions[index].Files, filesToLoad, filesToUnload);

                _groupsToLoad = new List<string>();

                GetLoadLists(doUnload ? _levelDescriptions[_currentLevel].Groups : null, _levelDescriptions[index].Groups, _groupsToLoad, groupsToUnload);
            }

            Unload(filesToUnload, groupsToUnload);
            DispatchEvent(new LevelEvent(LevelEvent.LEVEL_UNLOADED_EVENT, _currentLevel));

            _currentLevel = index;
            _isLevelLoaded = true;

            _pendingFiles = 1;
            foreach (string filename in filesToLoad)
            {
                _pendingFiles++;
                if (_loadFileCallback != null)
                {
                    _loadFileCallback(filename, FinishLoad);
                }
                else
                {
                    AAGame.TemplateManager.LoadFile(filename, force);
                }
            }

            FinishLoad();
        }

        private void OnFileLoaded(Event e)
        {
            FinishLoad();
        }

        private void FinishLoad()
        {
            _pendingFiles--;

            if (_pendingFiles > 0)
            {
                return;
            }

            foreach (string groupName in _groupsToLoad)
            {
                if (_loadGroupCallback != null)
                {
                    _loadGroupCallback(groupName);
                    continue;
                }

                IEnumerable<IEntity> groupEntities = AAGame.TemplateManager.InstantiateGroup(groupName);
                if (groupEntities == null)
                {
                    Logger.Error(this, "FinishLoad", "Failed to instantiate the group '" + groupName + "'.");
                    continue;
                }

                _loadedGroups[groupName] = new List<IEntity>();
                foreach (IEntity entity in groupEntities)
                {
                    _loadedGroups[groupName].Add(entity);
                }
            }

            _groupsToLoad = null;
            DispatchEvent(new LevelEvent(LevelEvent.LEVEL_LOADED_EVENT, _currentLevel));
        }

        /// <summary>
        /// Loads the next level after the current level.
        /// </summary>
        public void LoadNextLevel()
        {
            LoadLevel(_currentLevel + 1);
        }

        /// <summary>
        /// Unloads all the currently loaded data. Do not use this when another level is being loaded. Allow
        /// the Load() method to handle unloading old data.
        /// </summary>
        public void UnloadCurrentLevel()
        {
            if (!_isLevelLoaded)
            {
                Logger.Error(this, "UnloadCurrentLevel", "No level is loaded.");
                return;
            }

            List<string> filesToUnload = new List<string>();
            GetLoadLists(_levelDescriptions[_currentLevel].Files, null, null, filesToUnload);

            List<string> groupsToUnload = new List<string>();
            GetLoadLists(_levelDescriptions[_currentLevel].Groups, null, null, groupsToUnload);

            Unload(filesToUnload, groupsToUnload);

            _currentLevel = -1;
            _isLevelLoaded = false;

            DispatchEvent(new LevelEvent(LevelEvent.LEVEL_UNLOADED_EVENT, _currentLevel));
        }

        private void Unload(List<string> filesToUnload, List<string> groupsToUnload)
        {
            foreach (IEntity entity in _loadedEntities)
            {
                entity.EventDispatcher.RemoveEventListener(Entity.ENTITY_DESTROYED_EVENT, OnEntityDestroyed);
                entity.Destroy();
            }
            _loadedEntities.Clear();

            foreach (string groupName in groupsToUnload)
            {
                if (_unloadGroupCallback != null)
                {
                    _unloadGroupCallback(groupName);
                    continue;
                }

                foreach (IEntity entity in _loadedGroups[groupName])
                {
                    if (entity != null)
                    {
                        entity.Destroy();
                    }
                }

                if (groupName != AAGame.RootGroup.Name)
                {
                    AAGroup actualGroup = AAGame.NameManager.Lookup(groupName) as AAGroup;
                    if (actualGroup != null)
                    {
                        actualGroup.Destroy();
                    }
                }

                _loadedGroups.Remove(groupName);
            }

            foreach (string filename in filesToUnload)
            {
                if (_unloadFileCallback != null)
                {
                    _unloadFileCallback(filename);
                    continue;
                }

                AAGame.TemplateManager.UnloadFile(filename);
            }
        }

        private void GetLoadLists(List<string> prev, List<string> next, List<string> load, List<string> unload)
        {
            if (next != null)
            {
                foreach (string name in next)
                {
                    if (prev != null && prev.Contains(name))
                    {
                        continue;
                    }

                    load.Add(name);
                }
            }

            if (prev != null)
            {
                foreach (string name in prev)
                {
                    if (next != null && next.Contains(name))
                    {
                        continue;
                    }

                    unload.Add(name);
                }
            }
        }

        private void OnEntityDestroyed(Event e)
        {
        }

        /// <summary>
        /// Register a file with a level number. The same file can be registered with several levels.
        /// </summary>
        /// <param name="index">The level to register with</param>
        /// <param name="fileName">The file to register</param>
        public void AddFileReference(int index, string fileName)
        {
            if (_isLevelLoaded && (_currentLevel == index))
            {
                Logger.Error(this, "AddFileReference", "Cannot add level information to a level that is loaded.");
                return;
            }

            LevelDescription level = GetLevelDescription(index);
            level.Files.Add(fileName);
        }

        /// <summary>
        /// Remove a file from a level number.
        /// </summary>
        /// <param name="index">The level to remove from</param>
        /// <param name="filename">The file to remove</param>
        public void RemoveFileReference(int index, string filename)
        {
            if (!HasLevelData(index))
            {
                Logger.Error(this, "RemoveFileReference", "No level data exists for level '" + index + "'.");
                return;
            }

            LevelDescription level = GetLevelDescription(index);

            if (!level.Files.Contains(filename))
            {
                Logger.Error(this, "RemoveFileReference", "The file '" + filename + "' was not found in the level '" + index + "'.");
                return;
            }

            level.Files.Remove(filename);
        }

        /// <summary>
        /// Register a group with a level number. The same group can be registered with several levels.
        /// </summary>
        /// <param name="index">The level to register with</param>
        /// <param name="name">The name of the group to register</param>
        public void AddGroupReference(int index, string name)
        {
            if (_isLevelLoaded && (_currentLevel == index))
            {
                Logger.Error(this, "AddGroupReference", "Cannot add group information to a level that is loaded.");
                return;
            }

            LevelDescription level = GetLevelDescription(index);
            level.Groups.Add(name);
        }

        /// <summary>
        /// Remove a group from a level number.
        /// </summary>
        /// <param name="index">The level to remove from</param>
        /// <param name="name">The group to remove</param>
        public void RemoveGroupReference(int index, string name)
        {
            if (!HasLevelData(index))
            {
                Logger.Error(this, "RemoveGroupReference", "No level data exists for level '" + index + "'.");
                return;
            }

            LevelDescription level = GetLevelDescription(index);

            if (!level.Groups.Contains(name))
            {
                Logger.Error(this, "RemoveGroupReference", "The group '" + name + "' was not found in the level '" + index + "'.");
                return;
            }

            level.Groups.Remove(name);
        }

        /// <summary>
        /// Removes the level with the specified index.
        /// </summary>
        /// <param name="index">The index of the level to remove.</param>
        public void RemoveLevel(int index)
        {
            if (!HasLevelData(index))
            {
                Logger.Error(this, "RemoveGroupReference", "No level data exists for level '" + index + "'.");
                return;
            }

            _levelDescriptions.Remove(index);
        }

        public LevelDescription GetLevelDescription(int index)
        {
            if (!_levelDescriptions.ContainsKey(index))
            {
                LevelDescription level = new LevelDescription();
                level.Index = index;
                _levelDescriptions[index] = level;
            }

            return _levelDescriptions[index];
        }

        private bool HasLevelData(int index)
        {
            return _levelDescriptions.ContainsKey(index);
        }

        private bool _isReady = false;
        private bool _isLevelLoaded = false;
        private int _currentLevel = 0;
        private Dictionary<int, LevelDescription> _levelDescriptions = new Dictionary<int, LevelDescription>();

        private List<string> _groupsToLoad;
        private Dictionary<string, List<IEntity>> _loadedGroups = new Dictionary<string, List<IEntity>>();

        private List<IEntity> _loadedEntities = new List<IEntity>();

        private int _pendingFiles;
        private Action<string, Action> _loadFileCallback;
        private Action<string> _unloadFileCallback;
        private Action<string> _loadGroupCallback;
        private Action<string> _unloadGroupCallback;
    }

    public class LevelDescription
    {
        public int Index;
        public string Name;
        public List<string> Files = new List<string>();
        public List<string> Groups = new List<string>();
    }

    //public class LevelDescriptions
    //{
    //    [ContentSerializer(ElementName = "Levels", CollectionItemName = "Level")]
    //    public List<LevelDescription> Levels = new List<LevelDescription>();
    //}

    //public class LevelDescription
    //{
    //    public int Index;

    //    public string Name;
 
    //    [ContentSerializer(ElementName = "Files", CollectionItemName = "File")]
    //    public List<string> Files = new List<string>();

    //    [ContentSerializer(ElementName = "Groups", CollectionItemName = "Group")]
    //    public List<string> Groups = new List<string>();
    //}
}
